package com.itheima.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.dtos.ArticleInfoDto;
import com.heima.model.article.dtos.HotArticleVo;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleConfig;
import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.article.pojos.ApAuthor;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.constants.BehaviorConstants;
import com.itheima.Utils.UserThreadLocal;
import com.itheima.mapper.ArticleMapper;
import com.itheima.minio.utils.MinioUtils;
import com.itheima.service.ApAuthorService;
import com.itheima.service.AriticleConfigService;
import com.itheima.service.AriticleContentService;
import com.itheima.service.ArticleService;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import io.minio.errors.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 文章微服务业务实现类
 *
 * @author yunfei
 * @date 2022年10月14日 17:00
 */

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, ApArticle> implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private AriticleConfigService configService;

    @Autowired
    private AriticleContentService contentService;

    @Autowired
    private ApAuthorService authorService;

    @Autowired
    private Configuration configuration;

    @Autowired
    private MinioUtils minioUtils;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 默认加载数据
     *
     * @param homeDto
     * @return
     */
    @Override
    public ResponseResult load(ArticleHomeDto homeDto) {
        /*
            // 最大时间
            Date maxBehotTime;
            // 最小时间
            Date minBehotTime;
            // 分页size
            Integer size;
            // 频道ID
            String tag;

            //  默认加载
            select * from ap_article ap , ap_article_config ac
                where ap.id=ac.article_id and ap.channel_id = 1 and ap.publish_time <= 当前的系统时间
                and ac.is_down = 0  and ac.is_delete = 0

            // 下拉加载最新数据
            select * from ap_article ap , ap_article_config ac
                where ap.id=ac.article_id and ap.channel_id = 1 and ap.publish_time > maxBehotTime
                and ac.is_down = 0  and ac.is_delete = 0

            // 上来加载更多
            select * from ap_article ap , ap_article_config ac
                where ap.id=ac.article_id and ap.channel_id = 1 and ap.publish_time < minBehotTime
                and ac.is_down = 0  and ac.is_delete = 0
        *
        * */

        // 默认加载的时候要查询Redis
        // 如果Redis中没有数据  怎么办 ??
        // 直接查询MySQL把数据返回即可

        // 如果Redis中的数据 不足 人家要求的数据长度  怎么办 ??
        // 1. 先查询Redis  有多少数据就返回多少数据 然后再重新发请求查询MySQL(推荐)
        // 2. 先查询Redis  然后判断Redis中数据的长度 剩余的查询MySQL 再把数据拼接到一起返回

        // 如果默认查询的数据多了怎么办 再次加载更多的时候 怎么办 ??
        // 1. 先把Redis中的数据 按照人家要求的条数进行返回
        //    当用户上拉加载更多的时候 把Redis中的数据重新分条返回
        //    直到Redis中没有数据 再查询MySQL
        // 2. 先把Redis中的数据 按照人家要求的条数进行返回(推荐)
        //    当用户上拉加载更多的时候 直接查询MySQL

        // 设置一个接口 专门用来返回 热点数据  热点数据的长度要限定 (最合理的)

        // 校验size不能为空 也不能为负数
        if (ObjectUtil.isNull(homeDto.getSize()) || homeDto.getSize() <= 0) homeDto.setSize(10);

        // 获取要查询的频道类型
        // 如果频道id为空 我们默认查询的是 __all__ 查询所有文章
        // "HOTARTICLE_" + "__all__"
        String tag = homeDto.getTag();
        if (StrUtil.isEmpty(tag)) tag = "__all__";

        String s = redisTemplate.opsForValue().get("HOTARTICLE_" + tag);
        List<HotArticleVo> hotArticleVos = JSONUtil.toList(s, HotArticleVo.class);
        if (hotArticleVos.size() > homeDto.getSize()) hotArticleVos.subList(0, homeDto.getSize());
        return ResponseResult.okResult(hotArticleVos);
    }

    /**
     * 下拉加载最新数据
     *
     * @param homeDto
     * @return
     */
    @Override
    public ResponseResult loadnew(ArticleHomeDto homeDto) {
        // 校验size不能为空 也不能为负数
        if (ObjectUtil.isNull(homeDto.getSize()) || homeDto.getSize() <= 0) homeDto.setSize(10);
        if (ObjectUtil.isNull(homeDto.getTag())) return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        return ResponseResult.okResult(articleMapper.loadNew(homeDto));
    }

    /**
     * 上拉加载更多
     *
     * @param homeDto
     * @return
     */
    @Override
    public ResponseResult loadmore(ArticleHomeDto homeDto) {
        // 校验size不能为空 也不能为负数
        if (ObjectUtil.isNull(homeDto.getSize()) || homeDto.getSize() <= 0) homeDto.setSize(10);
        if (ObjectUtil.isNull(homeDto.getTag())) return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        return ResponseResult.okResult(articleMapper.loadMore(homeDto));
    }

    /**
     * 发布文章
     *
     * @param articleDto
     * @return
     */
    @Override
    public ResponseResult saveArticle(ArticleDto articleDto) throws IOException, TemplateException, InvalidResponseException, InvalidKeyException, NoSuchAlgorithmException, ServerException, ErrorResponseException, XmlParserException, InvalidBucketNameException, InsufficientDataException, InternalException {
        // 对数据做校验 文章的标题 文章的内容 文章作者 文章的频道
        if (!ObjectUtil.isAllNotEmpty(articleDto, articleDto.getTitle(), articleDto.getContent(), articleDto.getChannelName())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 保存数据到ap_article表中
        ApArticle apArticle = new ApArticle();
        BeanUtil.copyProperties(articleDto, apArticle, "content");
        // 根据user_id查询作者的id和作者的名称
        ApAuthor author = authorService.getOne(Wrappers.lambdaQuery(ApAuthor.class).eq(ApAuthor::getUserId, articleDto.getAuthorId()));
        apArticle.setAuthorName(author.getName());
        save(apArticle);


        // 保存数据到config表中
        ApArticleConfig config = new ApArticleConfig();
        config.setArticleId(apArticle.getId());
        // 是否允许评论
        config.setIsComment(true);
        // 是否允许转发
        config.setIsForward(true);
        // 是否已经删除
        config.setIsDelete(false);
        // 是否已经下架
        config.setIsDown(false);
        configService.save(config);


        // 保存数据到content表中
        ApArticleContent content = new ApArticleContent();
        content.setArticleId(apArticle.getId());
        content.setContent(articleDto.getContent());
        contentService.save(content);

        // 生成当前页面的静态资源页面
        //加载模板文件
        Template template = configuration.getTemplate("article.ftl");
        // 把文件直接上传到Minio中
        //输出 freemarker的数据模型的类型只能是map或是JavaBean对象
        HashMap<Object, Object> map = new HashMap<>();
        map.put("content", JSONUtil.parseArray(content.getContent()));
        StringWriter sw = new StringWriter();
        template.process(map, sw);

        // 上传静态页面到minio
        String htmlName = DateTime.now().toString("yyyy/MM/dd") + "/" + apArticle.getId() + ".html";
        minioUtils.putObject("", htmlName, new ByteArrayInputStream(sw.toString().getBytes()), "text/html");
        // 手动拼接一个静态资源的访问地址
        String staticUrl = minioUtils.getBasisUrl() + htmlName;
        // 把静态地址更新到文章表中
        apArticle.setStaticUrl(staticUrl);
        updateById(apArticle);
        return ResponseResult.okResult(apArticle.getId());
    }

    /**
     * 根据文章的id查询文章的详情
     *
     * @param articleId
     * @return
     */
    @Override
    public ResponseResult findOneById(long articleId) {
        /*
            "authorId": long"

            "authorName": "text"

            "content":"text",

            "id": "long"

            "images": "keyword",

            "layout": "integer"

            "publishTime" "date"

            "staticUrl": "keyword",

            "title": "text",
       */
        // 根据文章的id查询文章的对象
        ApArticle article = getById(articleId);
        if (ObjectUtil.isNull(article)) return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        // 封装一个针对ES使用的Bean  其中包含文章的所有属性和文章的内容
        ArticleDto articleDto = new ArticleDto();
        BeanUtil.copyProperties(article, articleDto);
        // 根据文章的id查询content表中的文章内容
        ApArticleContent content = contentService.getOne(Wrappers.lambdaQuery(ApArticleContent.class).eq(ApArticleContent::getArticleId, article.getId()));
        // 将文章的内容保存到返回对象中
        articleDto.setContent(content.getContent());

        return ResponseResult.okResult(articleDto);
    }

    /**
     * 查询所有文章 已经上架 且 没有被删除的文章
     * 查询出来给添加到ES中
     *
     * @return
     */
    @Override
    public ResponseResult findAll() {
        // 查询所有文章
        List<ApArticle> articleList = list();
        // 封装结果集对象
        List<ArticleDto> articleDtos = new ArrayList<>();
        for (ApArticle apArticle : articleList) {
            ArticleDto articleDto = new ArticleDto();
            BeanUtil.copyProperties(apArticle, articleDto);
            // 根据文章的id查询content表中的文章内容
            ApArticleContent content = contentService.getOne(Wrappers.lambdaQuery(ApArticleContent.class)
                    .eq(ApArticleContent::getArticleId, apArticle.getId()));
            // 将文章的内容保存到返回对象中
            articleDto.setContent(content.getContent());
            articleDtos.add(articleDto);
        }

        return ResponseResult.okResult(articleDtos);
    }

    /**
     * 文章详情行为数据回显
     *
     * @param dto
     * @return
     */
    @Override
    public ResponseResult loadArticleBehavior(ArticleInfoDto dto) {

        // 检查参数
        if (dto == null || dto.getArticleId() == null || dto.getAuthorId() == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        //{ "isfollow": true, "islike": true,"isunlike": false,"iscollection": true }
        boolean isfollow = false, islike = false, isunlike = false, iscollection = false;

        //点赞行为
        islike = redisTemplate.opsForHash().hasKey(BehaviorConstants.USER_BEHAVIOR + UserThreadLocal.get(),
                BehaviorConstants.LIKE_BEHAVIOR + dto.getArticleId());

        //不喜欢的行为
        isunlike = redisTemplate.opsForHash().hasKey(BehaviorConstants.USER_BEHAVIOR + UserThreadLocal.get(),
                BehaviorConstants.UN_LIKE_BEHAVIOR + dto.getArticleId());

        //是否收藏
        iscollection = redisTemplate.opsForHash().hasKey(BehaviorConstants.USER_BEHAVIOR + UserThreadLocal.get(),
                BehaviorConstants.COLLECTION_BEHAVIOR + dto.getArticleId());

        //是否关注
        Double score = redisTemplate.opsForZSet().score(BehaviorConstants.APUSER_FOLLOW_RELATION + UserThreadLocal.get(),
                dto.getAuthorId());
        isfollow = ObjectUtil.isNotNull(score);


        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("isfollow", isfollow);
        resultMap.put("islike", islike);
        resultMap.put("isunlike", isunlike);
        resultMap.put("iscollection", iscollection);

        return ResponseResult.okResult(resultMap);
    }

    /**
     * 查询最近五天的数据
     *
     * @return
     */
    @Override
    public ResponseResult selectFiveDaysArticle() {
        // 查询最近5天的数据 没有被下架 没有被删除
        List<ApArticle> articleList = list(Wrappers.lambdaQuery(ApArticle.class)
                .gt(ApArticle::getPublishTime, DateTime.now().offset(DateField.DAY_OF_MONTH, -5))
                .orderByDesc(ApArticle::getPublishTime));

        // 过滤上面的文章中已经被下架或者被删除的文章
        List<ApArticle> articles = new ArrayList<>();
        for (ApArticle apArticle : articleList) {
            ApArticleConfig config = configService.getOne(Wrappers.lambdaQuery(ApArticleConfig.class)
                    .eq(ApArticleConfig::getArticleId, apArticle.getId())
                    .eq(ApArticleConfig::getIsDown, false)
                    .eq(ApArticleConfig::getIsDelete, false));
            if (ObjectUtil.isNotNull(config)) articles.add(apArticle);
        }

        // 这个集合中的数据要保存到Redis
        List<HotArticleVo> hotArticleVoList = new ArrayList<>();
        // 这个Map集合是为了给频道进行分组
        Map<Integer, List<HotArticleVo>> vosMap = new HashMap<>();
        // 计算每个文章的得分
        for (ApArticle article : articles) {
            // 每个文章的分数
            Integer articleScore = 0;
            // 阅读数
            if (ObjectUtil.isNotNull(article.getViews())) articleScore += article.getViews();

            // 点赞数
            if (ObjectUtil.isNotNull(article.getLikes())) articleScore += article.getLikes() * 3;

            // 评论数
            if (ObjectUtil.isNotNull(article.getComment())) articleScore += article.getComment() * 5;

            // 收藏数
            if (ObjectUtil.isNotNull(article.getCollection())) articleScore += article.getCollection() * 8;

            HotArticleVo hotArticleVo = new HotArticleVo();
            BeanUtil.copyProperties(article, hotArticleVo);
            hotArticleVo.setScore(articleScore);
            hotArticleVoList.add(hotArticleVo);

            List<HotArticleVo> hotArticleVos = vosMap.get(hotArticleVo.getChannelId());
            if (CollUtil.isEmpty(hotArticleVos)) {
                hotArticleVos = new ArrayList<>();
                hotArticleVos.add(hotArticleVo);
                vosMap.put(hotArticleVo.getChannelId(), hotArticleVos);
            } else {
                hotArticleVos.add(hotArticleVo);
            }
        }

        // 把没有根据频道分组之前的数据先存储一份到Redis 目的是为了满足 推荐的查询
        redisTemplate.opsForValue().set("HOTARTICLE_" + "__all__",
                JSONUtil.toJsonStr(hotArticleVoList.subList(0, hotArticleVoList.size() >= 30 ? 30 : hotArticleVoList.size())), 1, TimeUnit.DAYS);

        // 按照频道进行分类 并且把每个分类下的数据保存到Redis中 每个分类下的数据不能超过30条
        for (Integer integer : vosMap.keySet()) {
            List<HotArticleVo> hotArticleVos = vosMap.get(integer);
            if (hotArticleVos.size() > 30) {
                List<HotArticleVo> hotArticleVos1 = hotArticleVos.subList(0, 30);
                hotArticleVos = hotArticleVos1;
            }
            // 存储Redis
            redisTemplate.opsForValue().set("HOTARTICLE_" + integer, JSONUtil.toJsonStr(hotArticleVos), 1, TimeUnit.DAYS);
        }

        return ResponseResult.okResult(null);
    }

}
